﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Web.Services.Description;
using System.Web.Services;
using System.Net;
using System.IO;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Xml.Serialization;
using System.Xml.Schema;
using System.Diagnostics;
using System.Collections.ObjectModel;

namespace CrmServiceGenerator
{
    class CrmServiceOptimizer
    {



        

        private List<string> _RemoveEntities;
        /// <summary>
        /// List of Entities (and associated classes) to remove
        /// </summary>
        public ReadOnlyCollection<string> RemoveEntities
        {
            get
            {
                return _RemoveEntities.AsReadOnly();
            }
        }

        private List<string> _KeepEntities;
        /// <summary>
        /// List of Entities (and associated classes) to keep
        /// </summary>
        public ReadOnlyCollection<string> KeepEntities
        {
            get
            {
                return _KeepEntities.AsReadOnly();
            }
        }

        /// <summary>
        /// Set to true to remove extra backend / UI types
        /// </summary>
        public bool RemoveBackendTypes { get; set; }
        /// <summary>
        /// Set to true to remove extra scheduling types
        /// </summary>
        public bool RemoveScedulingTypes { get; set; }

        public string AssemblyName { get; set; }

        public string FrameworkVersion { get; set; }

        public bool GenerateCode { get; set; }

        public bool EmbedXmlSerializers { get; set; }

        public string GenerateLanguage { get; set; }

        public bool Compile64 { get; set; }




        /// <summary>
        /// Main ServiceDescription for the WSDL
        /// </summary>
        private ServiceDescription serviceDescription;
        /// <summary>
        /// Reference to items list for main schema
        /// </summary>
        private XmlSchemaObjectCollection MainSchemaItems;
        /// <summary>
        /// Reference to EntityName enum
        /// </summary>
        private XmlSchemaSimpleTypeRestriction EntityNameEnum;

        /// <summary>
        /// Mapping from Type name to XmlSchemaType entry
        /// </summary>
        private Dictionary<string, XmlSchemaType> TypeList;

        /// <summary>
        /// Mapping from element name to XmlSchemaElement entry
        /// </summary>
        private Dictionary<string, XmlSchemaElement> ElementList;

        /// <summary>
        /// List of types to delete.
        /// </summary>
        private List<string> TypesToDelete;
        /// <summary>
        /// list of elements to delete.
        /// </summary>
        private List<string> ElementsToDelete;



        //Lists
        private List<MessageDef> MessageList;
        private List<string> BackendTypes;
        private List<string> SchedulingTypes;

        private CodeDomProvider domProvider;
        private CodeNamespace domNamespace;
        private CodeCompileUnit domCompileUnit;


        /// <summary>
        /// Initialize new CrmServiceOptimizer with reference lists
        /// </summary>
        /// <param name="messagePath">Path to file containing message definitions</param>
        /// <param name="backendPath">Path to file containing Backend Type list</param>
        /// <param name="schedulingPath">Path to file containing Scheduling Type list</param>
        public CrmServiceOptimizer(string messagePath, string backendPath, string schedulingPath)
        {
            

            TextReader tr = new StreamReader(messagePath);
            XmlSerializer s = new XmlSerializer(typeof(List<MessageDef>));
            MessageList = (List<MessageDef>)s.Deserialize(tr);
            tr.Close();

            TextReader tr2 = new StreamReader(backendPath);
            XmlSerializer s2 = new XmlSerializer(typeof(List<String>));
            BackendTypes = (List<String>)s2.Deserialize(tr2);
            tr2.Close();

            TextReader tr3 = new StreamReader(schedulingPath);
            XmlSerializer s3 = new XmlSerializer(typeof(List<String>));
            SchedulingTypes = (List<String>)s3.Deserialize(tr3);
            tr3.Close();
        }

        /// <summary>
        /// Load service description from local file
        /// </summary>
        /// <param name="path">path to file</param>
        public void LoadFile(string path)
        {
            serviceDescription = ServiceDescription.Read(path);
            internalLoad();
        }

        /// <summary>
        /// Load service description from url using default credentials
        /// </summary>
        /// <param name="url">url of wdsl</param>
        public void LoadUrl(string url)
        {
            using (WebClient wcWSDL = new WebClient())
            {
                wcWSDL.UseDefaultCredentials = true;
                serviceDescription = ServiceDescription.Read(new MemoryStream(wcWSDL.DownloadData(url)));
            }
            internalLoad();
        }

        /// <summary>
        /// Load service description from url using specified credentials
        /// </summary>
        /// <param name="url">wsdl url</param>
        /// <param name="webCredentials">credentials</param>
        public void LoadUrl(string url, ICredentials webCredentials)
        {
            using (WebClient wcWSDL = new WebClient())
            {
                wcWSDL.Credentials = webCredentials;
                serviceDescription = ServiceDescription.Read(new MemoryStream(wcWSDL.DownloadData(url)));
            }
            internalLoad();
        }


        /// <summary>
        /// Adds the specified entity to the Keep list, removing it from the Remove list
        /// </summary>
        /// <param name="entityName">Entity Name</param>
        public void AddRequiredEntity(string entityName, bool sort = true) 
        {
            if (_RemoveEntities.Contains(entityName))
            {
                _KeepEntities.Add(entityName);
                _RemoveEntities.Remove(entityName);
                if (sort) _KeepEntities.Sort();
            }
        }

        /// <summary>
        /// Adds all entity names in enumerable collection to keep list, removing from remove list
        /// </summary>
        /// <param name="entityNames">List of entity names</param>
        public void AddRequiredEntities(IEnumerable<string> entityNames)
        {
            foreach (string eName in entityNames)
            {
                AddRequiredEntity(eName,false);
            }
            _KeepEntities.Sort();
        }

        /// <summary>
        /// Adds the specified entity to the remove list, removing it from the keep list
        /// </summary>
        /// <param name="entityName">Entity Name</param>
        public void RemoveRequiredEntity(string entityName, bool sort = true)
        {
            if (_KeepEntities.Contains(entityName))
            {
                _RemoveEntities.Add(entityName);
                _KeepEntities.Remove(entityName);
                if (sort) _RemoveEntities.Sort();
            }
            
        }

        /// <summary>
        /// Adds all entity names in enumerable collection to remove list, removing from keep list
        /// </summary>
        /// <param name="entityNames">List of entity names</param>
        public void RemoveRequiredEntities(IEnumerable<string> entityNames)
        {
            foreach (string eName in entityNames)
            {
                RemoveRequiredEntity(eName,false);
            }
            _RemoveEntities.Sort();
        }


        private void internalLoad()
        {
            MainSchemaItems = serviceDescription.Types.Schemas["http://schemas.microsoft.com/crm/2007/WebServices"].Items;

            TypeList = new Dictionary<string, XmlSchemaType>();
            ElementList = new Dictionary<string, XmlSchemaElement>();

            foreach (XmlSchemaObject itemObj in MainSchemaItems)
            {
                if (itemObj.GetType() == typeof(XmlSchemaComplexType) || itemObj.GetType() == typeof(XmlSchemaSimpleType))
                {
                    XmlSchemaType xt = (XmlSchemaType)itemObj;
                    TypeList.Add(xt.Name.ToLower(), xt);
                }
                else if (itemObj.GetType() == typeof(XmlSchemaElement))
                {
                    XmlSchemaElement xe = (XmlSchemaElement)itemObj;
                    ElementList.Add(xe.Name.ToLower(), xe);
                }
            }

            //This is to keep the EntityName enum intact
            TypeList.Remove("additemcampaignrequest");


            EntityNameEnum = (XmlSchemaSimpleTypeRestriction)((XmlSchemaSimpleType)TypeList["entityname"]).Content;

            _RemoveEntities = new List<string>(EntityNameEnum.Facets.Count);
            _KeepEntities = new List<string>();

            foreach (XmlSchemaEnumerationFacet f in EntityNameEnum.Facets)
            {
                _RemoveEntities.Add(f.Value);
            }

            _RemoveEntities.Remove("none");
            _RemoveEntities.Sort();
        }



        private void UpdateTypesLists()
        {
            TypesToDelete = new List<string>(MessageList.Count * RemoveEntities.Count);
            ElementsToDelete = new List<string>();

            //Build messagelist

            foreach (string eName in RemoveEntities)
            {
                foreach (MessageDef mdef in MessageList)
                {
                    if (TypeList.Keys.Contains(mdef.Pre + eName + mdef.Post))
                    {
                        TypesToDelete.Add(mdef.Pre + eName + mdef.Post);
                    }
                }
            }


            if (RemoveBackendTypes)
            {
                TypesToDelete.AddRange(BackendTypes);
                ElementsToDelete.Add("arrayofsdkmessageprocessingstepregistration");
            }

            if (RemoveScedulingTypes)
            {
                TypesToDelete.AddRange(SchedulingTypes);
                ElementsToDelete.Add("validationresult");
                ElementsToDelete.Add("searchresults");
                ElementsToDelete.Add("arrayofvalidationresult");
                ElementsToDelete.Add("arrayoftimecode");
                ElementsToDelete.Add("arrayoftimeinfo");
                ElementsToDelete.Add("arrayofarrayoftimeinfo");
                ElementsToDelete.Add("appointmentrequest");
            }

            TypesToDelete.Sort();
        }



        private CodeDomProvider getDomProvider(string dotNetVersion, string language)
        {

            string versionString = "";

            switch (dotNetVersion)
            {
                case ".NET Framework 4.0":
                    versionString = "v4.0";
                    break;
                case ".NET Framework 3.5":
                    versionString = "v3.5";
                    break;
                case ".NET Framework 3.0":
                    versionString = "v3.0";
                    break;
                case ".NET Framework 2.0":
                    versionString = "v2.0";
                    break;

                default:
                    throw new InvalidOperationException("Unknown Framework specified.");
            }

            Dictionary<string, string> providerOpts = new Dictionary<string, string>();
            providerOpts.Add("CompilerVersion", versionString);

            if (language == "C#")
            {
                return new Microsoft.CSharp.CSharpCodeProvider(providerOpts);
            }
            else if (language == "Visual Basic")
            {
                return new Microsoft.VisualBasic.VBCodeProvider(providerOpts);
            }
            else
            {
                // maybe a try catch?
                return CodeDomProvider.CreateProvider(language);
            }

        }


        private void ValidateWsdl()
        {
            serviceDescription.Types.Schemas.Compile(ValHandler, true);


        }

        private void ValHandler(object Sender, ValidationEventArgs e)
        {
            log(e.Message);
        }

        private void AddXmlSerializerAssembly()
        {
            if (EmbedXmlSerializers)
            {
                foreach (CodeTypeDeclaration ctd in domNamespace.Types)
                {
                    if (ctd.Name == "CrmService")
                    {
                        ctd.CustomAttributes.Add(new CodeAttributeDeclaration("System.Xml.Serialization.XmlSerializerAssemblyAttribute"));
                        break;
                    }
                }

            }
            else
            {
                foreach (CodeTypeDeclaration ctd in domNamespace.Types)
                {
                    if (ctd.Name == "CrmService")
                    {
                        ctd.CustomAttributes.Add(new CodeAttributeDeclaration("System.Xml.Serialization.XmlSerializerAssemblyAttribute", new CodeAttributeArgument("AssemblyName", new CodePrimitiveExpression(AssemblyName + ".XmlSerializers"))));
                        break;
                    }
                }
            }


        }

        public void Optimize()
        {
            UpdateTypesLists();
            DoOptimization();

            log("Setting up CodeDom");


            domProvider = getDomProvider(FrameworkVersion, GenerateLanguage);
            domNamespace = new CodeNamespace(AssemblyName);
            domCompileUnit = new CodeCompileUnit();

            //setup the wsdl importer

            ServiceDescriptionImporter importer = new ServiceDescriptionImporter();
            importer.ProtocolName = "Soap12";
            //importer.ProtocolName = "Soap";
            importer.AddServiceDescription(serviceDescription, null, null);
            importer.Style = ServiceDescriptionImportStyle.Client;
            importer.CodeGenerationOptions = System.Xml.Serialization.CodeGenerationOptions.GenerateProperties;
            importer.CodeGenerator = domProvider;

            log("Attempting import");
            //attempt import

            try
            {
                ServiceDescriptionImportWarnings warning = importer.Import(domNamespace, domCompileUnit);

                if (warning != 0)
                {
                    log("Error: " + warning.ToString());
                    return;
                }
            }
            catch (Exception ex)
            {
                log("Error importing modified service!");
                log(ex.Message);
                log("Running Validator for details...");
                ValidateWsdl();
                MessageBox.Show("Fatal error(s) encountered while generating classes. Check the log for more information" + Environment.NewLine + "You probably need to including dependent entities, or uncheck backend / scheduling removal");
                return;
            }


            log("Import was a Success!");

            log("Adding XmlSerializerAssembly attribute");
            AddXmlSerializerAssembly();

            domCompileUnit.Namespaces.Add(domNamespace);

            if (GenerateCode)
            {
                log("Generating code");
                using (TextWriter outCode = new StreamWriter(domNamespace.Name + "." + domProvider.FileExtension))
                {
                    domProvider.GenerateCodeFromCompileUnit(domCompileUnit, outCode, new CodeGeneratorOptions());
                    outCode.Close();
                }
            }


            CompilerParameters cp = new CompilerParameters();
            cp.GenerateExecutable = false;
            cp.GenerateInMemory = false;
            cp.TreatWarningsAsErrors = false;

            if (Compile64)
            {
                cp.CompilerOptions = "/optimize+ /debug- /platform:x64 /keyfile:\"key.snk\"";
            }
            else
            {
                cp.CompilerOptions = "/optimize+ /debug- /keyfile:\"key.snk\"";
            }


            cp.OutputAssembly = this.domNamespace.Name + ".dll";
            log("Compiling");
            CompilerResults cr = domProvider.CompileAssemblyFromDom(cp, domCompileUnit);


            if (cr.Errors.Count > 0)
            {
                log("Build Errors:");
                foreach (CompilerError ce in cr.Errors)
                {
                    log(ce.ToString());
                }
                return;
            }

            log("Build Success!");



            DoSgen();


            if (EmbedXmlSerializers)
            {
                //TextReader sgenReader = new StreamReader(GetSgenSource());
                string sgenSource = GetSgenSource();
                //CodeCompileUnit sgenCompileUnit = domProvider.Parse(sgenReader);
                //CompilerResults cr2 = domProvider.CompileAssemblyFromDom(cp, domCompileUnit, sgenCompileUnit);
                CompilerResults cr2 = domProvider.CompileAssemblyFromFile(cp, domNamespace.Name + "." + domProvider.FileExtension, sgenSource);
                if (cr2.Errors.Count > 0)
                {
                    log("Build Errors:");
                    foreach (CompilerError ce in cr2.Errors)
                    {
                        log(ce.ToString());
                    }
                    return;
                }


            }




        }



        string GetSgenSource()
        {
            //look for all files *.cmdline, grab the newest one and find the \BLAHBLAH.cs, return this file as a stream.
            DirectoryInfo di = new DirectoryInfo(Environment.CurrentDirectory);
            FileInfo[] files = di.GetFiles("*.cmdline");

            if (files.Length > 0)
            {
                FileInfo NewestFile = files[0];
                foreach (FileInfo fi in files)
                {
                    if (fi.CreationTimeUtc > NewestFile.CreationTimeUtc)
                    {
                        NewestFile = fi;
                    }
                }

                string cmdContents = File.ReadAllText(NewestFile.ToString());

                int slashpos = cmdContents.LastIndexOf("\\");

                return cmdContents.Substring(slashpos + 1, (cmdContents.Length - slashpos) - 2);


            }
            else
            {
                return null;
            }

        }


        public void DoSgen()
        {
            log("Preparing Sgen");

            string sgenPath = "";
            string pfiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);

            if (FrameworkVersion == ".NET Framework 4.0")
            {
                if (Compile64)
                {
                    sgenPath = pfiles + "\\Microsoft SDKs\\Windows\\v7.0A\\Bin\\NETFX 4.0 Tools\\x64\\sgen.exe";
                }
                else
                {
                    sgenPath = pfiles + "\\Microsoft SDKs\\Windows\\v7.0A\\Bin\\NETFX 4.0 Tools\\sgen.exe";
                }
            }
            else
            {
                if (Compile64)
                {
                    sgenPath = pfiles + "\\Microsoft SDKs\\Windows\\v7.0A\\Bin\\x64\\sgen.exe";
                }
                else
                {
                    sgenPath = pfiles + "\\Microsoft SDKs\\Windows\\v7.0A\\Bin\\sgen.exe";
                }
            }


            if (!File.Exists(sgenPath))
            {
                MessageBox.Show("Could not find sgen.exe for your specified target!");
                return;
            }

            int sgenExitCode = 1;
            string sgenErrorMessage = "";

            try
            {
                Process p = new Process();
                p.StartInfo.FileName = sgenPath;
                if (EmbedXmlSerializers)
                {
                    p.StartInfo.Arguments = "/p /n /c:/keyfile:key.snk /force /keep " + AssemblyName + ".dll";
                }
                else
                {
                    p.StartInfo.Arguments = "/p /n /c:/keyfile:key.snk /force " + AssemblyName + ".dll";
                }

                p.StartInfo.WorkingDirectory = Environment.CurrentDirectory;
                p.StartInfo.UseShellExecute = false;
                p.StartInfo.CreateNoWindow = true;
                p.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
                p.StartInfo.RedirectStandardError = true;
                p.Start();
                sgenErrorMessage = p.StandardError.ReadToEnd();
                p.WaitForExit();
                sgenExitCode = p.ExitCode;
                log("sgen exit code: " + p.ExitCode);
                p.Close();
            }
            catch (Exception e)
            {
                log("Error running sgen batch file");
                log(e.Message);
                return;
            }

            if (sgenExitCode > 0)
            {
                log("Sgen Encountered an error");
                log(sgenErrorMessage);
                return;
            }



        }

        public TextBox txtLog;

        public void log(string message, bool newLine = true)
        {
            if (newLine)
            {
                txtLog.AppendText(DateTime.Now.ToString("HH:mm:ss.ff") + " " + message + Environment.NewLine);
            }
            else
            {
                txtLog.AppendText(DateTime.Now.ToString("HH:mm:ss.ff") + " " + message);
            }



            txtLog.SelectionStart = txtLog.TextLength;
            txtLog.ScrollToCaret();

            Application.DoEvents();
        }



        private void DoOptimization()
        {
            //Remove all specified types
            foreach (string t in TypesToDelete)
            {
                MainSchemaItems.Remove(TypeList[t]);
            }

            //remove specified elements
            foreach (string ee in ElementsToDelete)
            {
                MainSchemaItems.Remove(ElementList[ee]);
            }

            //Finally, remove removed types from the EntityName enum
            for (int i = EntityNameEnum.Facets.Count - 1; i >= 0; i--)
            {
                XmlSchemaEnumerationFacet thisFacet = (XmlSchemaEnumerationFacet)EntityNameEnum.Facets[i];
                if (RemoveEntities.Contains(thisFacet.Value))
                {
                    EntityNameEnum.Facets.RemoveAt(i);
                }
            }

            if (RemoveScedulingTypes)
            {
                // remove scheduling schema
                XmlSchema SchedulingSchema = serviceDescription.Types.Schemas["http://schemas.microsoft.com/crm/2006/Scheduling"];
                serviceDescription.Types.Schemas.Remove(SchedulingSchema);
            }

        }

    }



    [Serializable()]
    public struct MessageDef
    {
        public string Pre;
        public string Post;
        public MessageDef(string pre, string post)
        {
            Pre = pre;
            Post = post;
        }
    }


}
